﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

using CronusModel;
using Cronus.ViewModel.Data;
using Cronus.Utils;

namespace Cronus.ViewModel
{
    /// <summary>
    /// Internal singleton pointer to the data model.
    /// </summary>
    public class DataModel : IDisposable
    {
        //*** Constants ***//
        //*** Static ***//

        //*** Member Variables ***//
        CronusData _CronusDataSet = null;
        DataModelConfiguration _DataModelConfiguration;
        SingleThreadWorkPump _WorkerPump;
        bool _SavePending;

        //*** Properties ***//
        internal CronusData.ProjectsDataTable ProjectsTable
        {
            get { return _CronusDataSet.Projects; }
        }

        public IEnumerable<Project> Projects
        {
            get { return ProjectsTable.Select(projectRow => new Project(projectRow)); }
        }

        internal CronusData.TasksDataTable TasksTable
        {
            get { return _CronusDataSet.Tasks; }
        }

        public IEnumerable<Task> Tasks
        {
            get { return TasksTable.Select(taskRow => new Task(taskRow)); }
        }

        public IEnumerable<Task> UnmappedTasks
        {
            get { return Tasks.Where(task => task.Project == null); }
        }

        internal CronusData.ActivitiesDataTable ActivitiesTable
        {
            get { return _CronusDataSet.Activities; }
        }

        public IEnumerable<Activity> Activities
        {
            get { return ActivitiesTable.Select(activityRow => new Activity(activityRow)); }
        }

        /// <summary>
        /// Returns TRUE if the user is currently timing a task.
        /// </summary>
        public bool IsTiming
        {
            get 
            {
                var activitiesQuery = new QueryActivityInformation(this);
                return activitiesQuery.GetActivitiesBeingTimed().Count() == 0;
            }
        }

        /// <summary>
        /// Number of tasks currently in the database.
        /// </summary>
        public int NumberOfTasks
        {
            get { return TasksTable.Count; }
        }

        //*** Constructors ***//
        public DataModel(DataModelConfiguration aDataModelConfiguration)
        {
            _CronusDataSet = new CronusData();
            _DataModelConfiguration = aDataModelConfiguration;
            _WorkerPump = SingleThreadWorkPump.Create();
            _SavePending = false;
        }

        //*** Abstract Methods ***//
        //*** Methods ***//
        /// <summary>
        /// Performs required initialization on the DataModel after creation.
        /// </summary>
        public void InitializeDataModel()
        {
            // Local variables
            string strFullFilePath = String.Empty;

            try
            {
                // Get the full file path and load the file if it exists.
                strFullFilePath = _DataModelConfiguration.FullFilePath;
                if (File.Exists(strFullFilePath))
                {
                    _CronusDataSet.ReadXml(strFullFilePath);
                }

                // Setup event handlers for addition/modification/deletion of data.
                _CronusDataSet.Tasks.TasksRowChanged += OnDataSetChanged;
                _CronusDataSet.Tasks.TasksRowDeleted += OnDataSetChanged;
                _CronusDataSet.Activities.ActivitiesRowChanged += OnDataSetChanged;
                _CronusDataSet.Activities.ActivitiesRowDeleted += OnDataSetChanged;
                _CronusDataSet.Projects.ProjectsRowChanged += OnDataSetChanged;
                _CronusDataSet.Projects.ProjectsRowDeleted += OnDataSetChanged;
            }
            catch (Exception e)
            {
                // Create a backup of the bad file.  Just in case the user wants it later?  May be able to recover it?
                // TODO: Specific DataSet behavior.  Move this into separate area?
                if (String.Empty != strFullFilePath)
                {
                    DateTime aCurrentTime = DateTime.Now;

                    FileInfo aFileInfo = new FileInfo(strFullFilePath);

                    string strName = String.Format("({0})_{1}", aCurrentTime.ToString("yyyy.MM.dd_HH.mm.ss"), aFileInfo.Name);
                    string strNewFileName = String.Format("{0}{1}", aFileInfo.DirectoryName, strName);

                    // Create a back up of the file, TRUE to overwrite a file of the same name.
                    aFileInfo.CopyTo(strNewFileName, true);
                }

                // Re-throw the exception since we've handled making a copy of the file.
                throw (e);
            }
        }

        /// <summary>
        /// Handler when the dataset has changed and needs to be saved.  Generally the user may be
        /// performing actions and as the dataset grows the save operation is larger and takes more
        /// time.  Much more efficient to do this not quite immediately and on a separate thread.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnDataSetChanged(object sender, EventArgs e)
        {
            _WorkerPump.AddTask(() => {
                if (!_SavePending)
                {
                    _SavePending = true;
                    _WorkerPump.AddTaskLater(_DataModelConfiguration.SaveTimeMilliseconds, () => {
                        _CronusDataSet.WriteXml(_DataModelConfiguration.FullFilePath);
                        _SavePending = false;
                    });
                }
            });
        }

        /// <summary>
        /// Clean up as this object is being freed.  Singleton instance so this is called when the
        /// program is being shut down.
        /// </summary>
        public void Dispose()
        {
            // Save the dataset, but block until the pump has successfully finished saving the dataset.
            var evnt = new AutoResetEvent(false);
            _WorkerPump.AddTask(() => {
                _CronusDataSet.WriteXml(_DataModelConfiguration.FullFilePath);
                evnt.Set();
            });
            evnt.WaitOne();
            _WorkerPump.StopRunning(); // Stop the pump from running any more tasks.
        }

        /// <summary>
        /// Stops timing any Activities.
        /// </summary>
        public void StopTiming()
        {
            ModifyDataSet(() => {
                var taskActivities = new TaskActivities(this);
                taskActivities.StopTiming();
            }, () => { }, HandleException);
        }

        /// <summary>
        /// Starts timing the given task. If the Task does not already exist, the task is created.
        /// </summary>
        /// <param name="taskName">Task to start timing.</param>
        /// <returns>Activity object used to track timing.</returns>
        public void StartTiming(string taskName)
        {
            ModifyDataSet(() => {
                var taskActivity = new TaskActivities(this);
                taskActivity.StartTiming(taskName);
            }, () => { }, HandleException);
        }

        /// <summary>
        /// Creates a project with the given name if it doesn't already exist. Calls whenDone() when completed.
        /// </summary>
        /// <param name="projectName">Project name to create.</param>
        /// <param name="whenDone">Action called when completed.</param>
        public void CreateProject(string projectName, Action whenDone)
        {
            ModifyDataSet(() => {
                Project.Create(this, projectName);
            }, whenDone, HandleException);
        }

        /// <summary>
        /// Removes a project with the given name if it exists. Also removes associated tasks and events.
        /// </summary>
        /// <param name="projectName">Project to remove.</param>
        /// <param name="whenDone">Called when completed.</param>
        public void RemoveProject(string projectName, Action whenDone)
        {
            ModifyDataSet(() => {
                var project = Projects.FirstOrDefault(p => p.ProjectName == projectName);
                if (null != project)
                    project.Delete();
            }, whenDone, HandleException);
        }

        /// <summary>
        /// Associates the set of tasks with the project. Any task name that does not exist is ignored.
        /// </summary>
        /// <param name="projectName">Project to associate names with.</param>
        /// <param name="taskNames">Set of task names to associate with the project.</param>
        /// <param name="whenDone">Called when completed.</param>
        public void AssociateTasksWithProject(string projectName, IEnumerable<string> taskNames, Action whenDone)
        {
            ModifyDataSet(() => {

                // Find the project we need to associate
                var project = Projects.FirstOrDefault(p => p.ProjectName == projectName);
                if (null != project)
                    Project.AssociateTasksWithProject(this, project, taskNames);
            }, whenDone, HandleException);
        }

        /// <summary>
        /// Removes the association between the project and list of tasks. If any task is not associated with the project
        /// the task is ignored.
        /// </summary>
        /// <param name="projectName">Name of the project to remove the association from.</param>
        /// <param name="taskNames">Set of tasks to disassociate from the given project.</param>
        /// <param name="whenDone">Called when completed.</param>
        public void DisassociateTasksFromProject(string projectName, IEnumerable<string> taskNames, Action whenDone)
        {
            ModifyDataSet(() => {

                // Find the project.
                var project = Projects.FirstOrDefault(p => p.ProjectName == projectName);
                if (null != project)
                    Project.DisassociateTasksFromProject(project, taskNames);
            }, whenDone, HandleException);
        }

        /// <summary>
        /// Updates the current project name to the new project name.
        /// </summary>
        /// <param name="currentProjectName">Current project name.</param>
        /// <param name="newProjectName">New project name.</param>
        /// <param name="whenDone">Called when completed.</param>
        public void ModifyProjectName(string currentProjectName, string newProjectName, Action whenDone)
        {
            ModifyDataSet(() => {

                // Find the project by name, if it exists, update the name.
                var project = Projects.FirstOrDefault(p => p.ProjectName == currentProjectName);
                if (null != project)
                    Project.Rename(project, newProjectName);

            }, whenDone, HandleException);
        }

        /// <summary>
        /// Performs the datasetAction and commits the changes if successful. If an error occurs, changes are
        /// reverted and onError is called with the exception. whenDone() is always called.
        /// </summary>
        /// <param name="datasetAction">Action that modifies the dataset</param>
        /// <param name="whenDone">Called once everything is completed; always called.</param>
        /// <param name="onError">Called when an error occurs.</param>
        internal void ModifyDataSet(Action datasetAction, Action whenDone, Action<Exception> onError)
        {
            _WorkerPump.AddTask(() => {
                try
                {
                    // Run the given action that modifies the DataSet and then commit our changes.
                    datasetAction();
                    _CronusDataSet.AcceptChanges();
                }
                catch (Exception e)
                {
                    // Exception occurred, reject the changes and call the error function.
                    _CronusDataSet.RejectChanges();
                    onError(e);
                }

                whenDone();
            });
        }

        /// <summary>
        /// Performs the datasetAction and commits the changes if successful. whenDone() is always called.
        /// </summary>
        /// <param name="datasetAction">Action that modifies the dataset</param>
        /// <param name="whenDone">Called once everything is completed; always called.</param>
        internal void ModifyDataSet(Action datasetAction, Action whenDone)
        {
            ModifyDataSet(datasetAction, whenDone, HandleException);
        }

        /// <summary>
        /// Default error handling for executing a task in ModifyDataSet. Probably want to beef this up
        /// somehow.
        /// </summary>
        /// <param name="e">Exception that was caught.</param>
        void HandleException(Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}
